/* bbc-binary.vala
 *
 * Copyright (C) 2010, 2011  Richard Talbot-Watkins
 * Copyright (C) 2010, 2011  Dave Jeffery
 * 
 *  This program is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation, either version 3 of the License, or
 *  (at your option) any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
 * 
 * Authors:
 *  Richard Talbot-Watkins <richtw1@gmail.com>
 * 	Dave Jeffery <david.richard.jeffery@gmail.com>
 */

using Gee;
using Xml;

/**
 * Error domain for .bin/.bin.info import and export operations.
 */
public errordomain BeebSpriter.BBCBinaryError {
	WRITE_ERROR
}

/**
 * This struct is for holding bin.info name/hex value pairs.
 *
 * @author Richard Talbot-Watkins <richtw1@gmail.com>
 * @author Dave Jeffery <david.richard.jeffery@gmail.com>
 * @version 2011.0120
 * @since 0.1
 */
public class BeebSpriter.Symbol : GLib.Object {
	/* Name of sprite to be used in an assembler program.*/
	public string name { get; set; }

	/* Offset in bytes of sprite from beginning of .bin file.*/
	public int hex_value { get; set; }

	/**
	 * Contructor function for the Symbol struct.
	 *
	 * @param name Name an assembler should use to identify sprite.
	 * @param hex_value Offset of sprite's data from start of bin file.
	 */
	public Symbol (string name, int hex_value) {
		this.name = name;
		this.hex_value = hex_value;
	}
}

/**
 * This class allows BeebSpriter to write .bin and .bin.info files.
 *
 * @author Richard Talbot-Watkins <richtw1@gmail.com>
 * @author Dave Jeffery <david.richard.jeffery@gmail.com>
 * @version 2011.0118
 * @since 0.1
 */
public class BeebSpriter.BBCBinary : GLib.Object {
	/**
	 * Holds stream of bytes in BBC Micro format which is then exported.
	 */
	private uint8[] bbc_data;

	/**
	 * The sprite sheet to export in BBC Micro binary format.
	 */
	private SpriteSheet sprite_sheet;

	/**
	 * A list of symbols and indexes suitable for a 6502 assembler program.
	 */
	private ArrayList<Symbol> symbols;

	/**
	 * Saves sprites in a SpriteSheet as a binary file with header if needed.
	 *
	 * @param file The filename of the destination .bin BBC binary file.
	 */
	public void write (SpriteSheet sprite_sheet, File bin_file,
								File bin_info_file)	throws BBCBinaryError {

		this.sprite_sheet = sprite_sheet;
		prepare_bbc_data ();

		/* If the .bin file already exists, try and delete it. */
		try {
			if (bin_file.query_exists () && 
					bin_file.query_file_type (0) != FileType.DIRECTORY) {
				bin_file.delete ();
			}
		} catch (GLib.Error e) {
			var message = e.message;
			throw new BBCBinaryError.WRITE_ERROR (message);
		}

		/* Export binary data to a .bin file. */
		try {
			var file_stream = bin_file.create 
										(FileCreateFlags.REPLACE_DESTINATION);
			var data_stream = new DataOutputStream (file_stream);
			var length = bbc_data.length;

			for (int i = 0; i != length; ++i) {
				data_stream.put_byte (bbc_data[i]);
			}
		} catch (GLib.Error e) {
			var message = e.message;
			throw new BBCBinaryError.WRITE_ERROR (message);
		}

		/* Export binary data to a .bin.info file if required. */
		if (!sprite_sheet.should_generate_header) { return; }

		/* If the .bin_info file already exists, try and delete it. */
		try {
			if (bin_info_file.query_exists () && 
					bin_info_file.query_file_type (0) != FileType.DIRECTORY) {
				bin_info_file.delete ();
			}
		} catch (GLib.Error e) {
			var message = e.message;
			throw new BBCBinaryError.WRITE_ERROR (message);
		}

		/* Export binary data assembler header to a .bin.info file. */
		try {
			var file_stream = bin_info_file.create 
										(FileCreateFlags.REPLACE_DESTINATION);
			var data_stream = new DataOutputStream (file_stream);

			foreach (var symbol in symbols) {
				var output = sprite_sheet.assembler_syntax + "\n";
				output = output.replace("{n}", symbol.name);
				output = output.replace("{v}", symbol.hex_value.to_string ("%X"));
				data_stream.put_string (output);
			}

		} catch (GLib.Error e) {
			var message = e.message;
			throw new BBCBinaryError.WRITE_ERROR (message);
		}
	}

	/**
	 * Converts a sprite sheet into a stream of bytes and an ArrayList of
	 * 6502 assembler symbols and offset indexes.
	 */
	private void prepare_bbc_data () {
		symbols = new ArrayList<Symbol> ();
		bbc_data = {};
		var passes = (sprite_sheet.should_export_separate_mask) ? 2 : 1;
		var sprite_list = sprite_sheet.get_sprite_list ();

		for (var pass = 0; pass != passes; ++pass) {
			var offset = 0;
			foreach (Sprite s in sprite_list) {
				encode_sprite (s, pass);
				var size = bbc_data.length - offset;
				var mangled_name = mangle_name (s.name, pass);
				symbols.add (new Symbol (mangled_name + "_offset", offset));
				symbols.add (new Symbol (mangled_name + "_size", size));
				offset = bbc_data.length;
			}
		}
	}

	/**
	 * Mangles the name of a sprite (or its mask) into a format suitable for 
	 * a BBC Micro assembler symbol header file.
	 *
	 * @param string The sprite name to mangle.
	 * @param string Whether to mangle the name into a sprite or mask symbol.
	 * @returns A mangled name of a sprite or sprite mask.
	 */
	private string mangle_name (string name, int pass) {
		var to_mangle = name;
		if (to_mangle == "") { return to_mangle; }
		to_mangle = to_mangle.replace(" ", "_");
		if (pass == 1) { to_mangle += "_mask"; }
		return to_mangle;
	}

	/**
	 * Encodes a sprite (or its mask) in BBC Micro screen memory format.
	 *
	 * @param s Sprite instance to encode
	 * @param pass Whether the sprite or its mask should be encoded.
	 */
	private void encode_sprite (Sprite s, int pass) {
		var generate_mask = (pass == 1);
		var pixels_per_byte = sprite_sheet.pixels_per_byte;
		var sub_sprite_width = sprite_sheet.sub_sprite_width;
		var sub_sprite_height = sprite_sheet.sub_sprite_height;

		var num_pieces_across = 1;
		var num_pieces_down = 1;
		var piece_blocks_across = (s.width + pixels_per_byte - 1) 
															/ pixels_per_byte;
		var piece_height = s.height;

		if (sprite_sheet.should_break_sprites) {
			num_pieces_across = (piece_blocks_across + sub_sprite_width - 1)
														/ sub_sprite_width;
			num_pieces_down = (s.height + sub_sprite_height * 8 - 1)
													/ (sub_sprite_height * 8);
			piece_blocks_across = sub_sprite_width;
			piece_height = sub_sprite_height * 8;
		}

		/* Main export loop. */
		for (int piece_y = 0; piece_y != num_pieces_down; ++piece_y) {
			for (int piece_x = 0; piece_x != num_pieces_across; ++piece_x) {
				switch (sprite_sheet.sprite_layout) {
					case SpriteSheet.SpriteDataLayout.ROW_MAJOR:
						for (int y = 0; y < piece_height; y += 8) {
							for (int x = 0; x != piece_blocks_across; ++x) {
								for (int l = 0; l != 8; ++l) {
									uint8 beeb = 0x00;
									for (int p = 0; p != pixels_per_byte; 
																		++p) {
										var xx = 
										(piece_x * piece_blocks_across + x) * 
														pixels_per_byte + p;
										var yy = piece_y * piece_height 
																	+ y + l;
										var pixel = get_pixel 
												(s, xx, yy, generate_mask);
										beeb |= (uint8) 
													(get_beeb_colour (pixel) 
												<< (pixels_per_byte - 1 - p));
									}
									bbc_data += beeb;
								}
							}
						}
						break;
					case SpriteSheet.SpriteDataLayout.COLUMN_MAJOR:
						for (int x = 0; x != piece_blocks_across; ++x) {
							for (int y = 0; y != piece_height; ++y) {
								uint8 beeb = 0x00;
								for (int p = 0; p != pixels_per_byte; ++p) {
									var xx = 
									(piece_x + piece_blocks_across + x) * 
														pixels_per_byte + p;
									var yy = piece_y * piece_height + y;
									var pixel = 
										get_pixel (s, xx, yy, generate_mask);
									beeb |= (uint8) (get_beeb_colour (pixel) 
												<< (pixels_per_byte - 1 - p));
								}
								bbc_data += beeb;
							}
						}
						break;
				}
			}
		}
	}

	/**
	 * Returns the BBC physical colour for a pixel on a sprite (or its mask).
	 *
	 * @param sprite Sprite to get pixel from.
	 * @param x X-coordinate of pixel to get.
	 * @param y Y-coordinate of pixel to get.
	 * @param generate_mask Whether a mask or a sprite is being generated.
	 * @return BBC physical colour value.
	 */
	private uint8 get_pixel (Sprite sprite, int x, int y, 
														bool generate_mask) {
		uint8 pixel = 0xff;
		if (x >= 0 && x < sprite.width && y >= 0 && y < sprite.height) {
			pixel = sprite.bitmap[x + y * sprite.width];
		}

		if (generate_mask) {
			return (uint8) 
						((pixel == 0xff) ? sprite_sheet.num_colours - 1 : 0);
		} else {
			return (uint8) ((pixel == 0xff) ? 0 : pixel);
		}
	}

	/**
	 * Turns a BBC physical colour value as a BBC logical colour value.
	 *
	 * @param colour A BBC physical colour value.
	 * @return A BBC logical colour value.
	 */
	private uint8 get_beeb_colour (uint8 colour) {
		uint8 beeb_byte = 0x00;
		for (var i = 0; i != sprite_sheet.bits_per_pixel; ++i) {
			if ((colour & (1 << i)) != 0) {
				beeb_byte |= (uint8) (1 << (i * sprite_sheet.pixels_per_byte));
			}
		}
		return beeb_byte;
	}
}
